Day 16 我們已經加入 OrbitControls,現在可以直接使用滑鼠旋轉、縮放與平移整個太陽系。
不過目前還有一個很明顯的問題,所有行星都只是不同顏色的球。
例如地球現在其實只是 color: 0x2277ff
所以今天我們要開始使用 THREE.TextureLoader
把真正的行星圖片貼到球體表面。
今天會完成:
今天開始需要加入圖片,所以專案結構會多一個:
Modern-Web/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── main.js
└── textures/
xxx├── sun.jpg(太陽)
xxx├── mercury.jpg(水星)
xxx├── venus.jpg(金星)
xxx├── earth.jpg(地球)
xxx├── mars.jpg(火星)
xxx├── jupiter.jpg(木星)
xxx├── saturn.jpg(土星)
xxx├── uranus.jpg(天王星)
xxx└── neptune.jpg(海王星)
這樣 JavaScript 會比較好管理。
要注意,今天的程式是假設你的貼圖都放在 textures 裡面。
index.html
與 Day 16 相同,不需要修改。
css/style.css
與 Day 16 相同,不需要修改。(完整在 Day 9)
js/main.js
import * as THREE from
"three";
import { OrbitControls } from
"three/addons/controls/OrbitControls.js";
// ========================
// Scene
// ========================
const scene =
new THREE.Scene();
scene.background =
new THREE.Color(
0x000000
);
// ========================
// Camera
// ========================
const camera =
new THREE.PerspectiveCamera(
60,
window.innerWidth /
window.innerHeight,
0.1,
1000
);
camera.position.set(
30,
25,
30
);
camera.lookAt(
0,
0,
0
);
// ========================
// Renderer
// ========================
const renderer =
new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(
window.innerWidth,
window.innerHeight
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
document
.getElementById("scene")
.appendChild(
renderer.domElement
);
// ========================
// OrbitControls
// ========================
const controls =
new OrbitControls(
camera,
renderer.domElement
);
controls.enableDamping =
true;
controls.dampingFactor =
0.05;
controls.minDistance =
10;
controls.maxDistance =
80;
controls.target.set(
0,
0,
0
);
controls.update();
// ========================
// Clock
// ========================
const clock =
new THREE.Clock();
// ========================
// Texture Loader
// ========================
const textureLoader =
new THREE.TextureLoader();
// ========================
// Sun
// ========================
const sunTexture =
textureLoader.load(
"textures/sun.jpg"
);
const sunGeometry =
new THREE.SphereGeometry(
2,
64,
64
);
const sunMaterial =
new THREE.MeshBasicMaterial({
map: sunTexture
});
const sun =
new THREE.Mesh(
sunGeometry,
sunMaterial
);
scene.add(
sun
);
// ========================
// Planet Data
// ========================
const planetData = [
{
name: "Mercury",
size: 0.45,
distance: 4,
rotationSpeed: 0.8,
orbitSpeed: 1.2,
texture:
"textures/mercury.jpg"
},
{
name: "Venus",
size: 0.65,
distance: 6,
rotationSpeed: -0.3,
orbitSpeed: 0.95,
texture:
"textures/venus.jpg"
},
{
name: "Earth",
size: 0.7,
distance: 8,
rotationSpeed: 1.0,
orbitSpeed: 0.8,
texture:
"textures/earth.jpg"
},
{
name: "Mars",
size: 0.55,
distance: 10,
rotationSpeed: 0.9,
orbitSpeed: 0.65,
texture:
"textures/mars.jpg"
},
{
name: "Jupiter",
size: 1.3,
distance: 13,
rotationSpeed: 2.0,
orbitSpeed: 0.4,
texture:
"textures/jupiter.jpg"
},
{
name: "Saturn",
size: 1.1,
distance: 16,
rotationSpeed: 1.8,
orbitSpeed: 0.3,
texture:
"textures/saturn.jpg"
},
{
name: "Uranus",
size: 0.9,
distance: 19,
rotationSpeed: 1.2,
orbitSpeed: 0.22,
texture:
"textures/uranus.jpg"
},
{
name: "Neptune",
size: 0.85,
distance: 22,
rotationSpeed: 1.1,
orbitSpeed: 0.16,
texture:
"textures/neptune.jpg"
}
];
// ========================
// Create Orbits
// ========================
planetData.forEach(
(data) => {
const points = [];
const segments = 128;
for (
let i = 0;
i < segments;
i++
) {
const angle =
(i / segments) *
Math.PI * 2;
const x =
Math.cos(angle) *
data.distance;
const z =
Math.sin(angle) *
data.distance;
points.push(
new THREE.Vector3(
x,
0,
z
)
);
}
const orbitGeometry =
new THREE.BufferGeometry()
.setFromPoints(
points
);
const orbitMaterial =
new THREE.LineBasicMaterial({
color: 0x444444
});
const orbit =
new THREE.LineLoop(
orbitGeometry,
orbitMaterial
);
scene.add(
orbit
);
}
);
// ========================
// Create Planets
// ========================
const planets = [];
planetData.forEach(
(data, index) => {
// 載入行星貼圖
const planetTexture =
textureLoader.load(
data.texture
);
// 建立球體
const geometry =
new THREE.SphereGeometry(
data.size,
64,
64
);
// 使用貼圖材質
const material =
new THREE.MeshBasicMaterial({
map: planetTexture
});
// 建立行星
const planet =
new THREE.Mesh(
geometry,
material
);
scene.add(
planet
);
// 不同行星起始位置
const startAngle =
index * 0.8;
planets.push({
mesh: planet,
distance:
data.distance,
rotationSpeed:
data.rotationSpeed,
orbitSpeed:
data.orbitSpeed,
angle:
startAngle
});
}
);
// ========================
// Camera Controls
// ========================
window.addEventListener(
"keydown",
(event) => {
if (event.key === "1") {
camera.position.set(
0,
0,
40
);
}
if (event.key === "2") {
camera.position.set(
30,
25,
30
);
}
if (event.key === "3") {
camera.position.set(
0,
45,
0.1
);
}
if (event.key === "4") {
camera.position.set(
0,
12,
20
);
}
controls.target.set(
0,
0,
0
);
controls.update();
}
);
// ========================
// Resize
// ========================
window.addEventListener(
"resize",
() => {
camera.aspect =
window.innerWidth /
window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
}
);
// ========================
// Animation
// ========================
function animate() {
requestAnimationFrame(
animate
);
const delta =
clock.getDelta();
// 太陽自轉
sun.rotation.y +=
0.5 * delta;
planets.forEach(
(planet) => {
// 自轉
planet.mesh.rotation.y +=
planet.rotationSpeed *
delta;
// 公轉
planet.angle +=
planet.orbitSpeed *
delta;
planet.mesh.position.x =
Math.cos(
planet.angle
) *
planet.distance;
planet.mesh.position.z =
Math.sin(
planet.angle
) *
planet.distance;
}
);
controls.update();
renderer.render(
scene,
camera
);
}
animate();
今天第一個重要的新東西是:
const textureLoader =
new THREE.TextureLoader();
TextureLoader 顧名思義就是貼圖(Texture)
Loader 載入器 也就是專門幫我們載入圖片。
例如:
const sunTexture =
textureLoader.load(
"textures/sun.jpg"
);
代表載入:
textures/
└── sun.jpg
接著這張圖片就可以交給 Material 使用。
之前我們的材質是:
const material =
new THREE.MeshBasicMaterial({
color: data.color
});
代表整顆行星只有一種顏色,今天改成:
const material =
new THREE.MeshBasicMaterial({
map: planetTexture
});
這裡的 map 就是告訴 Material 把這張圖片貼到物件表面。
Day 16 的地球資料大概是:
{
name: "Earth",
size: 0.7,
distance: 8,
rotationSpeed: 1.0,
orbitSpeed: 0.8
}
今天變成:
{
name: "Earth",
size: 0.7,
distance: 8,
rotationSpeed: 1.0,
orbitSpeed: 0.8,
texture:
"textures/earth.jpg"
}
現在每顆行星又多了一個 貼圖位置
所以建立行星時:
const planetTexture =
textureLoader.load(
data.texture
);
就能自動載入對應圖片。
行星貼圖不能隨便拿一張圓形星球照片直接貼上去。
也就是把球體表面展開成長方形。
而本篇使用的素材是來自:
Solar System Scope 提供之 Solar Textures,
素材基於 NASA 影像與資料製作,並依 CC BY 4.0 授權使用。
點我前往 Solar System Scope
附圖
今天是整個專案視覺上非常重要的一天。
開始使用 THREE.TextureLoader
再搭配 map 把圖片貼到球體表面。
目前我們已經完成
全螢幕場景、太陽、八大行星、自轉、公轉、公轉軌道、不同運行速度、THREE.Clock、Camera、鍵盤切換視角、OrbitControls,以及行星 Texture。
另外從今天開始 Wireframe 就可以正式移除。
因為現在已經能直接從行星表面的圖案看出自轉。
「Sun Light:讓太陽真正成為光源」
目前雖然我們已經加入 Texture,但是材質使用的是THREE.MeshBasicMaterial
它有一個很重要的特性不受光線影響所以目前即使地球背對太陽整顆還是會一樣亮。
這不太像真正的太陽系。
Day 18 我們會開始加入 THREE.PointLight 把光源放在(0, 0, 0)也就是太陽的位置。
接著把行星材質從 MeshBasicMaterial 逐步改成會受到光線影響的材質。